Skip to content

fix(content): automatically recalculate offset when header/footer changes - #30862

Open
HappyKnuckles wants to merge 95 commits into
ionic-team:mainfrom
HappyKnuckles:fix/content-auto-offset
Open

HappyKnuckles wants to merge 95 commits into
ionic-team:mainfrom
HappyKnuckles:fix/content-auto-offset

Conversation

@HappyKnuckles

Copy link
Copy Markdown

Issue number: resolves #26981

What is the current behavior?

Currently, ion-content calculates its layout offsets (--offset-top/bottom) only on initialization or window resize.

If a developer dynamically changes the height of a header or footer (e.g., expanding a searchbar, wrapping text) or adds a toolbar conditionally to the DOM (e.g., via *ngIf), the ion-content does not adjust. This results in content being obscured behind the header/footer.

What is the new behavior?

This PR adds a ResizeObserver and MutationObserver to ion-content to automatically handle dynamic layout changes.

ResizeObserver watches sibling and elements and triggers a recalculation of --offset-top and --offset-bottom whenever their dimensions change.

MutationObserver watches the parent element to detect when or elements are added to or removed from the DOM.

Removes the need for developers to manually listen for standard dynamic layout changes.

Does this introduce a breaking change?

  • Yes
  • No

Other information

I have added a new E2E test suite in core/src/components/content/test/auto-offset which covers:

Visual Regression: Ensures no regressions on initial load.
Functional Header: Expands a header dynamically and asserts that content --offset-top update correctly.
Functional Footer: Expands a footer dynamically and asserts that content --offset-bottom update correctly.
Visual Confirmation: Snapshots verify that the content is physically pushed down/up when toolbars expand.

Before:
image

After:
image

@HappyKnuckles
HappyKnuckles requested a review from a team as a code owner December 11, 2025 19:01
@HappyKnuckles
HappyKnuckles requested a review from gnbm December 11, 2025 19:01
@vercel

vercel Bot commented Dec 11, 2025

Copy link
Copy Markdown

@HappyKnuckles is attempting to deploy a commit to the Ionic Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the package: core @ionic/core package label Dec 11, 2025

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your PR! This is looking really good! Aside from the broken tests, this is what I would like to see changed

Comment thread core/src/components/content/content.tsx Outdated
Comment thread core/src/components/content/content.tsx Outdated
Comment thread core/src/components/content/content.tsx Outdated
Comment thread core/src/components/content/content.tsx Outdated
@HappyKnuckles
HappyKnuckles requested a review from ShaneK January 24, 2026 23:37
@HappyKnuckles

Copy link
Copy Markdown
Author

@ShaneK any updates on this considering the review?

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm so sorry I didn't get back to this! I guess it was too close to Christmas and probably right before we took holiday break 🫠 I did notice some issues that I'm hoping won't be too hard to clean up, let me know if you need clarification

Comment on lines +443 to +448
if ('ResizeObserver' in window) {
let timeout: any;
this.resizeObserver = new ResizeObserver(() => {
clearTimeout(timeout);
timeout = setTimeout(() => this.resize(), 100);
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout here lives inside the ResizeObserver callback closure, so disconnectObservers can't reach it to call clearTimeout. If a header or footer resizes within ~100ms of the content being removed from the DOM, the pending setTimeout still fires and calls this.resize(), which calls forceUpdate on a disconnected component.

Could you hoist this to a class field (something like private observerResizeTimeout: ReturnType<typeof setTimeout> | null = null) and clear it in disconnectObservers? The existing resizeTimeout field at the top of the class is doing the same thing for the onResize handler at line 192.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I adjusted it in the latest commit.
However regarding the timeout: i noticed that the timeout is very noticeable visually.
Do you think it is worth switching to requestAnimationFrame?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Yeah, a rAF might be a better solution there, probably worth trying out. It should be a pretty quick test to see if it works better, basically the same structure, you'd even want to cache it and clear it out the same.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rAF indeed seems to improve it, so i will go with that solution.

Comment thread core/src/components/content/content.tsx Outdated
Comment thread core/src/components/content/content.tsx Outdated
Comment thread core/src/components/content/content.tsx Outdated
Comment thread core/src/components/content/test/auto-offset/content.e2e.ts Outdated
Comment thread core/src/components/content/test/auto-offset/content.e2e.ts Outdated
@HappyKnuckles
HappyKnuckles force-pushed the fix/content-auto-offset branch from 914cb59 to 95ddd56 Compare May 13, 2026 23:29
…nges

Previously, ion-content only calculated layout offsets on initialization or window resize. This caused content to be covered or obscured if a header or footer's dimensions changed dynamically (e.g. expanding a searchbar) or if they were added conditionally to the DOM.

This implementation adds a ResizeObserver to watch sibling headers/footers for size changes and a MutationObserver to detect when they are added, triggering an automatic recalculation of the --offset-top and --offset-bottom CSS variables.

Fixes ionic-team#26981
@HappyKnuckles
HappyKnuckles force-pushed the fix/content-auto-offset branch from 091ced7 to 0a2fc35 Compare May 19, 2026 16:55
@lincolnthree

Copy link
Copy Markdown

If it helps anyone, we are currently patching this in manually and it's working well for us! Thank you for the fix. Looking forward to seeing it merged.

@lincolnthree

Copy link
Copy Markdown

@HappyKnuckles Just checking with you on this. We're still manually patching this thanks to your hard work -- is the PR ready for review or is there something I could help with to un-block the team? Looks like it was ready to me (save maybe rebasing on main at this point) but just curious.

If it helps, I'd be happy to open a fresh PR with this rebased against main so you don't have to deal with the merge conflicts. Just let me know!

ShaneK and others added 12 commits October 3, 2026 23:52
…onic-team#31154)

Issue number: resolves ionic-team#25470

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
When an `<ion-tab-button>` declares an `href` with a query string or
fragment, activating the tab drops both. In Angular, `IonTabs.select`
navigates to `tabsPrefix/tab` and never reads the button's `href`. In
Vue, the router splits the path on `?` and discards everything after it,
and `IonTabBar` compares the raw href against the pathname so a tab with
query params never shows as selected. In React, this issue has been
fixed as part of the RR6 migration.

## What is the new behavior?
Tab activation forwards the `href`'s query and fragment as navigation
extras. A saved view's previously-captured extras still win when
re-selecting a tab with prior history, so mid-stack state isn't
clobbered. `IonTabBar`'s selection check now compares pathnames only, so
a tab with a query string still highlights when its pathname is active.

Added Playwright coverage in `@ionic/angular` and Cypress coverage in
`@ionic/vue` for first visit, switching tabs, switching back, and
re-clicking the active tab. The Vue tests will cause a conflict on
merging into major-9.0, but I volunteer to fix that issue when it comes
up.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

Preview pages:
- Angular:
https://ionic-framework-git-fw-7146-ionic1.vercel.app/angular/standalone/tabs-search-params/tab1?foo=bar
- Vue:
https://ionic-framework-git-fw-7146-ionic1.vercel.app/vue/tabs-search-params/tab1?foo=bar
- React: This was fixed in the RR6 migration in V9. Unfortunately this
won't be coming to 8.8 at this time.
…onic-team#31159)

Issue number: resolves ionic-team#27843

---------

## What is the current behavior?

`createInlineOverlayComponent` renders inline overlays (modal, popover,
etc.) inside a `<template>` at their declared JSX position. When the
overlay presents, `CoreDelegate` teleports the DOM node into `ion-app`,
but React's synthetic event delegation root stays at the original JSX
parent. Once the overlay lives outside that subtree, React no longer
dispatches events to children inside it, so `onClick`, `onChange`, and
other handlers inside an `IonModal` rendered within an `IonNav` silently
stop firing

## What is the new behavior?

Top-level inline overlays now render through `createPortal` into the
same `ion-app` container that `CoreDelegate` teleports into, so React's
event root follows the DOM

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

This issue also reports the same problem in Vue, but that was fixed in
ionic-team#30227

Preview:
[navigation-modal](https://ionic-framework-git-fw-6314-ionic1.vercel.app/react/navigation-modal)

Dev build:
```
8.8.8-dev.11779302602.17decfbf
```
Issue number: resolves internal

---------

## What is the current behavior?
Playwright tests are hanging because of a [bug that breaks it for node
24.16+](microsoft/playwright#40724)

## What is the new behavior?
Upgrading Playwright to 1.16.0 to fix the issue

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
…-team#31178)

Many of the controller tests are using the wrong path to import the
overlay controllers. This results in those tests not working in Vercel.

To reproduce the issue:

1. Open the [Action Sheet
(main)](https://ionic-framework-git-main-ionic1.vercel.app/src/components/action-sheet/test/basic)
preview
2. Attempt to open any Action Sheet
3. See the Action Sheet does not open and there is the following console
error: `ReferenceError: actionSheetController is not defined`

To see it working:

1. Open the [Action Sheet
(branch)](https://ionic-framework-git-test-vercel-controllers-ionic1.vercel.app/src/components/action-sheet/test/basic)
preview
2. Attempt to open any Action Sheet
3. See the Action Sheet opens without any errors

This should be able to reproduce on any of the updated tests.

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Issue number: resolves ionic-team#19566, resolves ionic-team#19365

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?

Clicking an `ion-router-link` whose `href` contains a `#fragment` (e.g.
`/catalog#pens`) fails to navigate and logs `not part of the routing
set`. `parsePath` in `core/src/components/router/utils/path.ts` only
splits on `?`, so the fragment gets folded into the last path segment
and no registered route matches. Even when navigation succeeds, the
matching anchor is never scrolled into view.

## What is the new behavior?

`parsePath` extracts the fragment as a separate field, and
`writeSegments` writes it back onto the URL via `pushState`. Routes
match against the path segments alone, so `/catalog#pens` resolves to
the `/catalog` route as expected.

After `writeNavStateRoot` resolves, the router polls a few animation
frames for an element matching the fragment inside the active page, then
smooth-scrolls to it via `ion-content.scrollToPoint`, falling back to
`Element.scrollIntoView` when the target sits outside `ion-content`. A
`fragmentScrollToken` cancels any in-flight scroll the moment a newer
`setSegments` runs.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

The fragment lookup scopes to the last `.ion-page:not(.ion-page-hidden)`
in the document. Nested outlets can have multiple non-hidden pages
mounted at once, and an identically-named anchor on a parent page would
otherwise win over the leaf the user is viewing.

Preview pages:
-
[router/basic](https://ionic-framework-git-fw-6958-ionic1.vercel.app/src/components/router/test/basic#/two)
Issue number: resolves stenciljs/core#6727

## What is the current behavior?

When a component property's default value is set to a JavaScript
variable (e.g. `QUERY['lg']` on `ion-split-pane`'s `when` prop), Stencil
emits the raw variable expression into the generated documentation files
(`api.txt`, `components.d.ts`, and `dist/docs.json`). This causes the
[component API
docs](https://ionicframework.com/docs/api/split-pane#prop-when) to
display `QUERY['lg']` instead of the resolved value `'(min-width:
992px)'`.

## What is the new behavior?

Stencil 4.43.5 (via stenciljs/core#6728) resolves variable references at
doc-generation time. After bumping and rebuilding:

- `core/api.txt` — `ion-split-pane`'s `when` default is now
`'(min-width: 992px)'`
- `core/src/components.d.ts` — `@default` JSDoc tag reflects the
resolved value
- `dist/docs.json` (published as `@ionic/docs`) — will also carry the
resolved value once released, enabling ionic-docs to remove the
hardcoded fallbacks for the `gallery` component

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

- Stencil changelog:
https://github.com/stenciljs/core/releases/tag/v4.43.5
- Upstream fix: stenciljs/core#6728
- Once this is released as part of Ionic, a PR will be automatically
created in `ionic-docs` using the updated `@ionic/docs` package — at
that point the hardcoded default descriptions for `gallery`'s `columns`
and `gap` props can be removed.
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@capacitor/core](https://capacitorjs.com)
([source](https://redirect.github.com/ionic-team/capacitor)) | [`8.3.4`
→
`8.4.0`](https://renovatebot.com/diffs/npm/@capacitor%2fcore/8.3.4/8.4.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fcore/8.4.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fcore/8.3.4/8.4.0?slim=true)
|

---

### Release Notes

<details>
<summary>ionic-team/capacitor (@&ionic-team#8203;capacitor/core)</summary>

###
[`v8.4.0`](https://redirect.github.com/ionic-team/capacitor/blob/HEAD/CHANGELOG.md#840-2026-06-02)

[Compare
Source](https://redirect.github.com/ionic-team/capacitor/compare/8.3.4...8.4.0)

##### Bug Fixes

- **android:** show only the requested system bar
([#&ionic-team#8203;8480](https://redirect.github.com/ionic-team/capacitor/issues/8480))
([4c6c321](https://redirect.github.com/ionic-team/capacitor/commit/4c6c3219afb5223211e857457e46283c37eb9424))
- **cli:** revert live reload config on failure
([#&ionic-team#8203;8485](https://redirect.github.com/ionic-team/capacitor/issues/8485))
([1d031a4](https://redirect.github.com/ionic-team/capacitor/commit/1d031a4abec2c793079ba8897ad2e40c4cc6c7f9))
- **SystemBars:** make `safe-area-inset-x` available on API <= 34
([#&ionic-team#8203;8424](https://redirect.github.com/ionic-team/capacitor/issues/8424))
([e456de0](https://redirect.github.com/ionic-team/capacitor/commit/e456de083e19644f484bec5a5359cb67960ac8bc))
- **SystemBars:** respect `insetsHandling` disable
([#&ionic-team#8203;8481](https://redirect.github.com/ionic-team/capacitor/issues/8481))
([d4ad7ff](https://redirect.github.com/ionic-team/capacitor/commit/d4ad7ffe39daf66e0cfc63af9028d5c05543bde7))

##### Features

- add method getDouble to plugin config
([#&ionic-team#8203;7638](https://redirect.github.com/ionic-team/capacitor/issues/7638))
([93c72de](https://redirect.github.com/ionic-team/capacitor/commit/93c72de40a2ec4c78b33659250cb08340083088e))
- **cli:** add experimental packageOptions
([#&ionic-team#8203;8471](https://redirect.github.com/ionic-team/capacitor/issues/8471))
([258867b](https://redirect.github.com/ionic-team/capacitor/commit/258867b7bf37b1837b99b02ec9638e5a6df08d97))
- **cli:** capture ios\_package\_manager in telemetry
([#&ionic-team#8203;8482](https://redirect.github.com/ionic-team/capacitor/issues/8482))
([b4b297a](https://redirect.github.com/ionic-team/capacitor/commit/b4b297a52f8732659662d5e5aaeff81c0f7d9835))

####
[8.3.4](https://redirect.github.com/ionic-team/capacitor/compare/8.3.3...8.3.4)
(2026-05-12)

**Note:** Version bump only for package capacitor

####
[8.3.3](https://redirect.github.com/ionic-team/capacitor/compare/8.3.2...8.3.3)
(2026-05-08)

##### Bug Fixes

- **cli:** copy plugin files in CocoaPods projects
([#&ionic-team#8203;8467](https://redirect.github.com/ionic-team/capacitor/issues/8467))
([b2d7719](https://redirect.github.com/ionic-team/capacitor/commit/b2d771926a180e60deea31992d7d4abcd5ca3bc7))

####
[8.3.2](https://redirect.github.com/ionic-team/capacitor/compare/8.3.1...8.3.2)
(2026-05-07)

##### Bug Fixes

- **cli:** add cSettings support for compiler flags in generated
Package.swift
([#&ionic-team#8203;8448](https://redirect.github.com/ionic-team/capacitor/issues/8448))
([0bd0676](https://redirect.github.com/ionic-team/capacitor/commit/0bd0676315c5fd77e50312dd7b5bf4990dcbd7d0))
- **cli:** add system framework and weak framework support in SPM
Package.swift
([#&ionic-team#8203;8447](https://redirect.github.com/ionic-team/capacitor/issues/8447))
([3232f0f](https://redirect.github.com/ionic-team/capacitor/commit/3232f0fe1d9811b0b5c500e3dc05cb8a250177f8))
- **cli:** correct Capacitor plugin SPM compat check
([#&ionic-team#8203;8440](https://redirect.github.com/ionic-team/capacitor/issues/8440))
([e5ccc45](https://redirect.github.com/ionic-team/capacitor/commit/e5ccc451dda27d56bca824ed644bd20fe4d988cb))
- **cli:** generate binaryTarget entries for custom xcframeworks in
Package.swift
([#&ionic-team#8203;8445](https://redirect.github.com/ionic-team/capacitor/issues/8445))
([1f7e33f](https://redirect.github.com/ionic-team/capacitor/commit/1f7e33fca43d183332ec19d22b0d75ef81d8cc6d))
- **cli:** generate resource entries in Package.swift
([#&ionic-team#8203;8455](https://redirect.github.com/ionic-team/capacitor/issues/8455))
([790bd27](https://redirect.github.com/ionic-team/capacitor/commit/790bd27123497111984227010c3162cec94a108e))
- **cli:** handle Cordova plugins without iOS source files
([#&ionic-team#8203;8443](https://redirect.github.com/ionic-team/capacitor/issues/8443))
([0da130e](https://redirect.github.com/ionic-team/capacitor/commit/0da130eb7a861bee4e2c35bc0aac53ba9c983fc3))
- **cli:** link plugin dependencies in Package.swift
([#&ionic-team#8203;8457](https://redirect.github.com/ionic-team/capacitor/issues/8457))
([b3c769e](https://redirect.github.com/ionic-team/capacitor/commit/b3c769e856c826b1174518877cf86ac7ce73bf09))
- **ios:** support Cordova plugins with Package.swift
([#&ionic-team#8203;8438](https://redirect.github.com/ionic-team/capacitor/issues/8438))
([139943b](https://redirect.github.com/ionic-team/capacitor/commit/139943b0c05fddb2d1ce2d6f468800fddf17b4cf))
- **SystemBars:** avoid extra view padding on API <= 34
([#&ionic-team#8203;8439](https://redirect.github.com/ionic-team/capacitor/issues/8439))
([5b135a7](https://redirect.github.com/ionic-team/capacitor/commit/5b135a70217be560e7176c8d5b514cc92ed3e4e4))

####
[8.3.1](https://redirect.github.com/ionic-team/capacitor/compare/8.3.0...8.3.1)
(2026-04-16)

##### Bug Fixes

- **android:** handle null versionName in isNewBinary()
([#&ionic-team#8203;8397](https://redirect.github.com/ionic-team/capacitor/issues/8397))
([aa1a660](https://redirect.github.com/ionic-team/capacitor/commit/aa1a660f364f9b5f5a1e350e279c8864b04dd13b))
- boundary value extraction for form-data requests
([#&ionic-team#8203;7518](https://redirect.github.com/ionic-team/capacitor/issues/7518))
([64ab854](https://redirect.github.com/ionic-team/capacitor/commit/64ab854c12330804c24275d88d3a9f7c8e52a73d))
- **cli:** check CAPACITOR\_COCOAPODS\_PATH in determinePackageManager
([#&ionic-team#8203;8407](https://redirect.github.com/ionic-team/capacitor/issues/8407))
([acb64ab](https://redirect.github.com/ionic-team/capacitor/commit/acb64ab92a37ff53701cde453558e272e2e11eb6))
- **system-bars:** use separate current styles
([#&ionic-team#8203;8409](https://redirect.github.com/ionic-team/capacitor/issues/8409))
([3d1f8d1](https://redirect.github.com/ionic-team/capacitor/commit/3d1f8d1b61480187375f5cd4de7ba999db007542))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "every weekday before 11am"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMDkuMCIsInVwZGF0ZWRJblZlciI6IjQzLjIwOS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…31190)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[pozil/auto-assign-issue](https://redirect.github.com/pozil/auto-assign-issue)
| action | major | `v3.0.0` → `v4.0.0` |

---

### Release Notes

<details>
<summary>pozil/auto-assign-issue (pozil/auto-assign-issue)</summary>

###
[`v4.0.0`](https://redirect.github.com/pozil/auto-assign-issue/compare/v3.0.0...v4.0.0)

[Compare
Source](https://redirect.github.com/pozil/auto-assign-issue/compare/v3.0.0...v4.0.0)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "every weekday before 11am"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMDkuMCIsInVwZGF0ZWRJblZlciI6IjQzLjIwOS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
renovate Bot and others added 23 commits October 3, 2026 23:52
…31319)

This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@capacitor/core](https://capacitorjs.com)
([source](https://redirect.github.com/ionic-team/capacitor)) | [`8.4.2`
→
`8.5.0`](https://renovatebot.com/diffs/npm/@capacitor%2fcore/8.4.2/8.5.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fcore/8.5.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fcore/8.4.2/8.5.0?slim=true)
|

---

### Release Notes

<details>
<summary>ionic-team/capacitor (@&ionic-team#8203;capacitor/core)</summary>

###
[`v8.5.0`](https://redirect.github.com/ionic-team/capacitor/blob/HEAD/CHANGELOG.md#850-2026-07-31)

[Compare
Source](https://redirect.github.com/ionic-team/capacitor/compare/8.4.2...8.5.0)

##### Bug Fixes

- **cli:** support TypeScript 7 when loading capacitor.config.ts
([#&ionic-team#8203;8534](https://redirect.github.com/ionic-team/capacitor/issues/8534))
([4c1c870](https://redirect.github.com/ionic-team/capacitor/commit/4c1c8709413b9c19b008c99122ca330cc3c90e6f))

##### Features

- **cli:** add migrator functionality for adopting UIScene
([#&ionic-team#8203;8544](https://redirect.github.com/ionic-team/capacitor/issues/8544))
([984fa85](https://redirect.github.com/ionic-team/capacitor/commit/984fa85ba0adab0aacf895aed6323bf4b503dccb))
- **ios:** UIScene Support
([#&ionic-team#8203;8536](https://redirect.github.com/ionic-team/capacitor/issues/8536))
([3fa04a3](https://redirect.github.com/ionic-team/capacitor/commit/3fa04a357c92af34cd6fccb8124791963804a9dc))

####
[8.4.2](https://redirect.github.com/ionic-team/capacitor/compare/8.4.1...8.4.2)
(2026-07-14)

##### Bug Fixes

- **android:** explicitly grant URI permissions for image capture intent
([#&ionic-team#8203;8526](https://redirect.github.com/ionic-team/capacitor/issues/8526))
([6f2d328](https://redirect.github.com/ionic-team/capacitor/commit/6f2d3283897a375d09ca9ec8784a9f0d65f0a530))

####
[8.4.1](https://redirect.github.com/ionic-team/capacitor/compare/8.4.0...8.4.1)
(2026-06-19)

##### Bug Fixes

- **cli:** make SPM dependency patch work on prereleases
([#&ionic-team#8203;8508](https://redirect.github.com/ionic-team/capacitor/issues/8508))
([6048e90](https://redirect.github.com/ionic-team/capacitor/commit/6048e90171afa0229a3c25b52a23c377c6bb804c))
- **cli:** patch Capacitor SPM dependency version in plugins
([#&ionic-team#8203;8492](https://redirect.github.com/ionic-team/capacitor/issues/8492))
([28bb2c6](https://redirect.github.com/ionic-team/capacitor/commit/28bb2c687069dfdd6aa7abc866004a1c6388d103))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "every weekday before 11am"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4zLjIiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Issue number: resolves ionic-team#31315 
---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

When an `ion-refresher`'s host page is unmounted while the native
refresher is still being set up, Ionic throws an uncaught `TypeError`
from the gesture utility:

```
TypeError: Cannot read properties of undefined (reading '__zone_symbol__addEventListener')
    at addEventListener (core/src/utils/gesture/listener.ts:21)
    at Object.enable (core/src/utils/gesture/index.ts)
    at Refresher.disabledChanged (core/src/components/refresher/refresher.tsx:125)
    at Refresher.setupMDNativeRefresher (core/src/components/refresher/refresher.tsx:372)
```

`setupMDNativeRefresher()` and `setupiOSNativeRefresher()` both do:

```ts
this.gesture = (await import('../../utils/gesture')).createGesture({
  el: this.scrollEl!,
  ...
});
```

The dynamic import yields to the event loop. If the refresher is
disconnected while it resolves, `disconnectedCallback()` (line 547-549)
has already run `this.scrollEl = undefined`, so `createGesture` receives
`el: undefined`, and the subsequent `disabledChanged()` →
`gesture.enable(true)` calls `addEventListener(undefined, ...)`, which
throws.

Because this happens inside an async method that nothing awaits, it
surfaces as an **unhandled promise rejection** — it cannot be caught by
a framework error boundary; this crashes my app in playwright sometimes
when components are mounted very quickly.

The `!` non-null assertions on `this.scrollEl` are what hide this from
TypeScript.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

If the refresher is disconnected while the gesture module is being
imported, setup aborts quietly. No gesture is created, and no error is
thrown.

## Does this introduce a breaking change?

- [ ] Yes
- [ X] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

---------

Co-authored-by: Shane <shane@shanessite.net>
…ken contributing link (ionic-team#31250)

Issue number: resolves ionic-team#31251

---------

## What is the current behavior?

While working on e2e tests in the angular directory, I encountered a few
documentation issues that made the local development workflow more
difficult to follow.

- The **"See Ionic's E2E testing guide"** link in `CONTRIBUTING.md`
resolves to a 404 when viewed from GitHub's **Contributing** tab because
the relative path is not resolved correctly.
- Broken link:
https://github.com/ionic-team/ionic-framework/blob/core/src/utils/test/playwright/docs/README.md
- Intended destination:
https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/test/playwright/docs/README.md

- The Angular testing guide (`docs/angular/testing.md`) omits several
steps that are necessary to successfully test local framework changes,
including:
- Removing existing `npm link` relationships before starting a new sync
cycle.
  - Building `packages/angular-server`.
- Installing Playwright browser dependencies before running Playwright
tests for the first time.

These omissions made it difficult to reproduce and validate additions to
Angular tests I was adding as part of a separate PR.

## What is the new behavior?

This PR does the following:

- Fixes the broken link to the Ionic E2E testing guide in
`CONTRIBUTING.md` by using a repository-root-relative path.
- Updating `docs/angular/testing.md` to document the complete workflow
for syncing local Angular framework changes.
- Documents that `packages/angular-server` should be built as part of
the local testing workflow.
- Documents that contributors should remove previous `npm link`
relationships before beginning a new sync cycle.
- Clarifies the recommended workflow for syncing local package changes.
- Documents the Playwright browser installation step required before
running Playwright tests for the first time.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

---------

Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
…am#31153)

Issue number: resolves ionic-team#31152

## What is the current behavior?

In `IonRouter.handleHistoryChange`, the URL-change guard compares
mismatched operands:

```ts
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
if (leavingUrl !== location.pathname) { ... }
```

The left side includes `search`, the right side does not. For any route
with a non-empty query string, the comparison is **always** unequal, so
the transition block runs on every history event — including no-op
popstates over same-URL entries pushed via `window.history.pushState`.

Concretely: when a same-URL history entry on a search-bearing route is
popped, `action === 'POP'` is processed and the IRO transitions to
`currentRoute.pushedByRoute`. The browser URL doesn't change but the
rendered view does — the user gets silently teleported to a different
page in the stack.

Minimal repro: on any search-bearing route, run in the console:
```js
window.history.pushState({}, '', window.location.href);
window.history.back();
```
The current behavior swaps the rendered page to the previous entry in
`locationHistory` while the URL stays put. Expected: no visible change.

Full repro and analysis in ionic-team#31152. This is also the root cause behind
the symptom reported in ionic-team#25534 (framed there as a transition-rerender
flash).

## What is the new behavior?

The right side of the comparison now also includes `search`, so the
block only runs when the URL actually changed:

```ts
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
const currentUrl = location.pathname + (location.search || '');
if (leavingUrl !== currentUrl) { ... }
```

Behavior matrix:
- Search-only navigations (e.g. `routerPush(samePath + newSearch)`):
**unchanged** — pathname matches but search differs, block still runs.
- Pathname changes: **unchanged** — pathnames differ, block still runs.
- No-op popstates on search-bearing routes: **fixed** — pathname +
search both match, block correctly skipped.

A new Cypress regression test is included
(`packages/react-router/test/base/tests/e2e/specs/routing.cy.js`) that
pushes a same-URL state on a search-bearing route, calls
`history.back()`, and asserts the page does not teleport.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

The comparison becomes stricter (skips the block in more cases than
before), but only for the cases where the URL didn't actually change.
All cases where the URL *did* change are still routed through the
existing transition logic unchanged.

## Other information

Happy to iterate on the fix shape if there's a preferred alternative —
e.g. gating behind a flag for backward compat, or restructuring the
guard differently. The one-line change above is the smallest possible
fix that keeps existing behavior for every "real URL change" case.

---------

Co-authored-by: ShaneK <shane@shanessite.net>
…ty (ionic-team#31249)

## Summary

Fixes ionic-team#18550 — `ion-fab-button` with `type="submit"` does not submit its
parent form.

**Root cause:** `ion-fab-button` uses Shadow DOM, so its internal
`<button>` element is not part of the form-associated elements in the
light DOM. Clicking it (or pressing Enter in a text field) never
triggers the form's `submit` event.

**Fix:** Port the hidden-button pattern already used by `ion-button`:
- On render (when `type !== 'button'`), inject a hidden `<button>` into
the associated form in the light DOM.
- On click, call `ev.preventDefault()` to stop the shadow button from
acting, then `.click()` the hidden button — which is a proper form
participant and triggers submission.
- Add a `form` prop (`string | HTMLFormElement`) so the button can
target a form outside its DOM subtree (same API as `ion-button`).
- Keep the `disabled` watcher in sync with the hidden button so a
disabled fab-button cannot submit.

## Changes

- `core/src/components/fab-button/fab-button.tsx` — core fix
- `core/src/components/fab-button/test/form/fab-button.e2e.ts` —
Playwright e2e tests (closest form, form by id, form by reference, Enter
key, disabled, reset)
- `core/src/components/fab-button/test/form/fab-button.spec.ts` — unit
test asserting the hidden button is not duplicated on re-renders
- `core/src/components/fab-button/test/form/index.html` — visual test
page

## Test plan

- [ ] `npm run test.spec` passes (unit test for hidden-button dedup)
- [ ] `npm run test.e2e` passes for
`fab-button/test/form/fab-button.e2e.ts`
- [ ] Manual: `ion-fab-button type="submit"` inside a `<form>` submits
on click
- [ ] Manual: pressing Enter in a form input triggers submit
- [ ] Manual: `ion-fab-button type="reset"` resets form fields
- [ ] Manual: disabled fab-button does not submit
- [ ] Manual: `form="id"` targets an external form

---------

Co-authored-by: ShaneK <shane@shanessite.net>
Issue number: internal

---------

## What is the current behavior?

Currently, `prepareLazyLoaded` in `ion-tab` sets `loaded = true` before
it attempts the attach, and the `try/catch` wrapped around
`attachComponent` is synchronous while `attachComponent` itself is
`async`. So when the first attach rejects, the rejection skips the
`catch` entirely and `loaded` is already `true`, which means the `if
(!this.loaded && ...)` guard blocks every later attempt. The tab renders
as an empty page for the rest of the session, even if whatever caused
the failure is fixed.

`ion-tabs` activates a tab by setting `active` rather than by awaiting
the tab's `setActive()`, so the failure goes through the
`@Watch('active')` path. That call isn't awaited, so on rejection the
error surfaces as an unhandled promise rejection and `printIonError`
never runs. `select()` resolves successfully while the tab is blank, so
there's no signal to the caller either.

## What is the new behavior?

With this change, `prepareLazyLoaded` caches the in-flight attach
promise instead of flipping a boolean up front. Concurrent activations
still share a single attempt, which is what the old `loaded = true`
ordering was providing, but the cache is only cleared when the attach
rejects, so the next activation retries. The unawaited call in the
`active` watcher now has a `.catch` that routes the error to
`printIonError`, which is what the unreachable `catch` block was there
for.

`setActive()` still rejects on a failed attach and still leaves `active`
as `false`, so that part is unchanged.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information

There's no stock test page that reproduces the failure, since it needs a
delegate that rejects, but the tabs pages confirm normal lazy loading
still works:

- [Tabs:
Basic](https://ionic-framework-git-fw-7296-ionic1.vercel.app/src/components/tabs/test/basic)
…-team#31335)

Issue number: resolves ionic-team#31155, resolves ionic-team#31143

---------

## What is the current behavior?

Currently, an `ion-datetime` inside a modal or popover shows the wrong
month once the overlay is reopened. The selected day isn't visible, the
previous month button does nothing, and picking a day from the grid
lands on an unrelated date.

Overlays move their host element into `ion-app` when presenting and back
to its original position when dismissing, which disconnects and
reconnects the datetime. `disconnectedCallback` reset
`hasBeenIntersecting` during that move, so by the time the hidden-state
`IntersectionObserver` entry arrived, `hiddenCallback` mistook the
dismissal for the synthetic initial entry and returned early. That left
`datetime-ready` on the host, so on the next present `markReady` saw the
class and returned without re-centering the calendar on the working
month, and the browser had already reset `scrollLeft` to 0 while the
overlay was hidden. `scrollLeft: 0` renders the previous month's grid
while the header still names the working month, which is what produces
all three symptoms.

## What is the new behavior?

With this change, `disconnectedCallback` no longer resets
`hasBeenIntersecting`. That flag tracks the observers, and the observers
are only created in `componentDidLoad` and never re-created on
reconnect, so a DOM move has no business clearing it. `hiddenCallback`
now sees the real hidden transition on dismiss and tears down as it did
before ionic-team#31108, which lets `markReady` run again on the next present and
re-center the calendar.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

This regression was introduced in ionic-team#31108. That PR needed the flag to
make `hiddenCallback` ignore the synthetic initial entry, but the
`disconnectedCallback` reset it also added had no job and broke the
overlay case. Before ionic-team#31108, `hiddenCallback` had no guard at all and
always removed `datetime-ready` on dismiss, so this restores the
behavior that shipped for all of v8..

- [Relevant test screen -
iOS](https://ionic-framework-git-fix-31155-ionic1.vercel.app/src/components/datetime-button/test/overlays?ionic:mode=ios)
- [Relevant test screen -
MD](https://ionic-framework-git-fix-31155-ionic1.vercel.app/src/components/datetime-button/test/overlays?ionic:mode=md)

To reproduce: open the "Modal - Default" picker, dismiss it, then open
it again. On `main` the grid shows February while the header reads March
2022.

---------

Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
…m#31323)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[github/codeql-action](https://redirect.github.com/github/codeql-action)
| action | patch | `v4.37.4` → `v4.37.5` |

---

### Release Notes

<details>
<summary>github/codeql-action (github/codeql-action)</summary>

###
[`v4.37.5`](https://redirect.github.com/github/codeql-action/compare/v4.37.4...v4.37.5)

[Compare
Source](https://redirect.github.com/github/codeql-action/compare/v4.37.4...v4.37.5)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "every weekday before 11am"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4zLjIiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…m#31340)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[github/codeql-action](https://redirect.github.com/github/codeql-action)
| action | patch | `v4.37.5` → `v4.37.6` |

---

### Release Notes

<details>
<summary>github/codeql-action (github/codeql-action)</summary>

###
[`v4.37.6`](https://redirect.github.com/github/codeql-action/releases/tag/v4.37.6)

[Compare
Source](https://redirect.github.com/github/codeql-action/compare/v4.37.5...v4.37.6)

- Changed the default filepath for the new remote file address format
that was introduced in CodeQL Action 4.37.0 / 3.37.0 to
`.github/codeql-config.yml` to align it with the suggested path that is
used elsewhere.
[#&ionic-team#8203;4070](https://redirect.github.com/github/codeql-action/pull/4070)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "every weekday before 11am"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Issue number: resolves ionic-team#31333

---------

## What is the current behavior?

Currently, Stencil's lazy loader resolves component bundles with a
template literal dynamic import wrapped in a `/* @vite-ignore */`
comment. Vite 8 moved dependency prebundling from esbuild to Rolldown,
which honors that comment and leaves the import alone, so the
`.entry.js` chunks never get emitted into the prebundle output. Requests
for them 404 and no lazy component registers, which surfaces as
`Constructor for "ion-app#undefined" was not found` in the console.

Angular CLI 22.1 is where most people hit this, since its dev server is
the first to ship Vite 8. It only affects the lazy `IonicModule` path,
because that's the only thing in the repo that pulls
`@ionic/core/loader`. Standalone components import from
`@ionic/core/components` and never touch the loader.

## What is the new behavior?

With this change we set `extras.enableImportInjection` in
`core/stencil.config.ts`. Stencil prepends a switch of literal import
paths to the loader, ahead of the existing `@vite-ignore` import, so
Rolldown can resolve each bundle and emit its chunk. The original import
stays as the fallback, so nothing changes for consumers that already
worked.

The fix is one config flag that would silently revert if anyone dropped
it, so there's a guard alongside it.
`core/scripts/verify/lazy-imports.js` reads the built `dist/esm` and
`dist/cjs`, locates the loader chunk, and fails if any bundle is missing
a literal import. It runs in both `build-core` and
`build-core-stencil-prerelease`, so a Stencil upgrade that changes the
injection behavior fails the nightly rather than shipping.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No


## Other information

Verified against a stock Angular CLI 22.1 app using
`IonicModule.forRoot()` and a local build of this branch. Before the fix
we had a ton of 404s on `ion-app_8.entry.js` and
`ion-button_2.entry.js`, plus some `Constructor for ... was not found`
errors. After the fix we had no console errors, components render fine,
and `ng build` emits the entry chunks with the right names.

CI doesn't cover the actual symptom and it cannot here because we have
no ng22 test apps on main, but we'll have it tested for free on
major-9.0 with the existing ng22 test app there. The guard script
asserts the build-output invariant for the extra validation.

The injected switch adds roughly 10.8 KB raw to the lazy runtime chunk,
which is under 1 KB gzipped because the case clauses are near-identical
repeated text, but people using the CDN build will have no extra
overhead at all.

Anyone on a published `@ionic/core` can work around this with
`prebundle.exclude` in `angular.json` until it ships:

```json
"serve": {
  "options": {
    "prebundle": { "exclude": ["@ionic/angular", "@ionic/core"] }
  }
}
```
…am#31339)

Issue number: resolves ionic-team#31332

---------

## What is the current behavior?

Currently, pulling down inside a sheet or card modal dismisses the modal
instead of triggering the refresher when the content uses a custom
scroll host.

## What is the new behavior?

With this change, the gesture resolves the refresher from the enclosing
`ion-content` when the swipe target is a custom scroll host, so pulling
down runs the refresher and leaves the modal alone. The new
`findRefresherInContent` util only reports a refresher for the scroll
host that refresher actually scrolls with, which is the first one in the
content, so a second unrelated scroll host still swipes the modal
normally. The `expandToScroll: false` path now runs the same check.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information

Test coverage is 4 e2e tests on the new page:

[Test page
(iOS)](https://ionic-framework-git-fix-31332-ionic1.vercel.app/src/components/modal/test/refresher-scroll-target/?ionic:mode=ios)
[Test page
(MD)](https://ionic-framework-git-fix-31332-ionic1.vercel.app/src/components/modal/test/refresher-scroll-target/?ionic:mode=md)

## Current dev build:

```
8.8.18-dev.11786385944.17aca7e5
```
…1313)

Issue number: resolves ionic-team#31312 

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
Current docs don't provide recommended testing guidance for accurately
testing standalone components. This can create issues because a
standalone (custom-elements) build doesn't expose
`el.componentOnReady()`, so any test that reads fixture.nativeElement
before hydration in a standalone project has the potential to not be
testing what they think they are.

## What is the new behavior?
- Adds a couple of sentences to talk about using the existing
`componentOnReady` helper from `@ionic/core` in Angular's `testing.md`
file
- Adds the same standalone guidance to the `testing.md` files for React
and Vue

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

---------

Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
…tep (ionic-team#31310)

Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

The Playwright npm packages and the `mcr.microsoft.com/playwright`
Docker image in `core/Dockerfile` are tracked by two separate Renovate
managers. The Docker image is published to the container registry about
a day after the npm release, so the two updates become available at
different times. Because they are on different datasources, the npm bump
opens a PR before the Docker tag is ready, and getting the Dockerfile
into the same PR requires a manual rebase once the image lands. This is
easy to miss, and merging the npm bump alone leaves `core/package.json`
and `core/Dockerfile` on mismatched Playwright versions.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- A custom manager now resolves the `core/Dockerfile` Playwright image
tag from the npm `@playwright/test` release instead of the container
registry, so both files bump from the same version.
- The built-in Docker manager for that image is disabled so it no longer
produces a separate, lagging update.
- The Dockerfile is added to the grouped `playwright` rule, and a
`minimumReleaseAge` of 3 days lets the matching image publish before the
PR opens.
- Net result is a single `playwright` PR that updates
`core/package.json` and `core/Dockerfile` together, with no manual
rebase.

## Does this introduce a breaking change?

- [ ] Yes
- [ ] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

This is a Renovate config change only, so it will not take effect until
the next scheduled Renovate run. If the Docker image for a given release
has not been published yet when CI runs, the Dockerfile build can fail
on a missing tag; the fix is to wait for the image to appear and re-run
the job, with no code change needed. This is documented inline in
`renovate.json5`.
…am#31343)

Issue number: resolves ionic-team#31337

---------

Currently, `ion-content` caches its fullscreen offsets and only
recomputes them on load, a `window` resize, `ionTabBarLoaded`, or
`recalculateDimensions()`. A change to `--ion-safe-area-top` fires none
of those, so on Android edge-to-edge, where the real inset only arrives
once the native WindowInsets callback has run, the fullscreen scroll
region and `#background-content` stay short by the inset and leave a gap
behind a translucent header. Sheet modals have the same problem:
`--ion-modal-offset-top` is a resolved pixel value written at present
time, so a sheet presented before the inset is known stays too tall.

Fullscreen `ion-content` now observes its own box with a
`ResizeObserver`, which works because the content flexes to fill
whatever space the headers and footers leave behind. The callback keeps
the `offsetParent === null` guard from `onResize()`, since a hidden page
reports a 0x0 box and recomputing then would zero the offsets, but it
skips that method's 100ms debounce, which left the offsets visibly wrong
for a small amount of time. Sheet modals subscribe to
`onRootSafeAreaTopChange()`, a hidden probe element whose `height`
tracks `var(--ion-safe-area-top)` so a change to the variable becomes an
observable size change.

- [ ] Yes
- [X] No

The preview pages won't show the bug on their own, since it needs the
inset to change after load. Run
`document.documentElement.style.setProperty('--ion-safe-area-top',
'24px')` in the console to trigger it.

- [Fullscreen content
(iOS)](https://ionic-framework-git-fix-31337-ionic1.vercel.app/src/components/content/test/fullscreen?ionic:mode=ios)
/ [Fullscreen content
(MD)](https://ionic-framework-git-fix-31337-ionic1.vercel.app/src/components/content/test/fullscreen?ionic:mode=md)
- [Modal safe-area
(iOS)](https://ionic-framework-git-fix-31337-ionic1.vercel.app/src/components/modal/test/safe-area?ionic:mode=ios)
/ [Modal safe-area
(MD)](https://ionic-framework-git-fix-31337-ionic1.vercel.app/src/components/modal/test/safe-area?ionic:mode=md)
…m#31357)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[github/codeql-action](https://redirect.github.com/github/codeql-action)
| action | patch | `v4.37.6` → `v4.37.7` |

---

### Release Notes

<details>
<summary>github/codeql-action (github/codeql-action)</summary>

###
[`v4.37.7`](https://redirect.github.com/github/codeql-action/releases/tag/v4.37.7)

[Compare
Source](https://redirect.github.com/github/codeql-action/compare/v4.37.6...v4.37.7)

- Update default CodeQL bundle version to
[2.26.3](https://redirect.github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.3).
[#&ionic-team#8203;4085](https://redirect.github.com/github/codeql-action/pull/4085)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "every weekday before 11am"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yNC4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjQuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…ic-team#31246)

Issue number: resolves ionic-team#30226

---------

## What is the current behavior?

When `dir="rtl"` is set on `<ion-app>` (but not on `document`), the
hamburger menu appears on the right side visually, but the **open
animation still slides in from the left**. This happens because
`isEndSide()` only checked `document.dir`, not the nearest ancestor
`dir` attribute.

## What is the new behavior?

- `isEndSide()` takes an optional host element and delegates to
`isRTL()` instead of reading `document.dir` itself.
- `isRTL()` now walks up from the given element to the nearest ancestor
that declares a `dir`, so `<ion-app dir="rtl">` applies to everything
inside it even when the document is `ltr`. It previously read only the
element's own `dir` before falling back to `document.dir`.
- `sideChanged()` in `ion-menu` also passes the menu element so the side
is correct before the first animation.
- Unit tests added for the `ion-app dir="rtl"` case.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Co-authored-by: Austin-Spriggs <aspriggs@madisoncollege.edu>
…eck (ionic-team#31363)

Issue number: resolves ionic-team#30539

---------

## What is the current behavior?

Currently, `supportsPassive` in the gesture listener utils detects
passive listener support by attaching a real `optsTest` listener to the
element and reading the `passive` getter, but it never removes that
listener. The result is a listener permanently attached to whichever
element runs the first gesture.

There's a second failure mode in the same function. The `_sPassive ===
undefined` guard only clears if the getter fires or if
`addEventListener` throws, so a runtime that does neither leaves
`_sPassive` undefined and every subsequent call attaches another
`optsTest` listener that's never cleaned up.

## What is the new behavior?

We now always pass the options object, and the detection is removed
entirely. The check was already dead code, because passive options
object support landed in Chrome 51, Safari 10, Firefox 49, and Edge 16,
all in 2016, while our lowest supported versions are Chrome 89, Safari
15, Firefox 75, and Edge 89. The function returned `true` on every
browser we support, so both branches of the removed ternary collapse to
the same value and there's no behavior change.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information
…31365)

Issue number: resolves ionic-team#31026

---------

Opening an `ion-range` with `dualKnobs` set to `true` with a value set
inside of a modal does not render the values properly.

- Added a watcher for `dualKnobs` that calls `updateRatio()`, ensuring
knob ratios are recomputed whenever `dualKnobs` changes. The `min`,
`max`, and `value` properties already had watchers. `dualKnobs` was the
only ratio-affecting property without one.
- Knobs are now positioned correctly regardless of whether `value` or
`dualKnobs` is assigned first. They are also repositioned correctly when
`dualKnobs` is toggled at runtime in either direction.
- Range now sets the correct values when used inside a modal, including
when `dualKnobs` and `value` are assigned in different orders.
- Improved the `dualKnobs` property description.
- Added e2e tests covering both property assignment orders and runtime
toggling of `dualKnobs`.

- [ ] Yes
- [x] No

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Co-authored-by: pratyushjaiswal0806-dot <pratyushjaiswal0806@gmail.com>
@HappyKnuckles
HappyKnuckles force-pushed the fix/content-auto-offset branch from de18daa to ed06d3c Compare October 3, 2026 23:14
@github-actions github-actions Bot added package: angular @ionic/angular package package: vue @ionic/vue package package: react @ionic/react package labels Oct 3, 2026
@HappyKnuckles

Copy link
Copy Markdown
Author

@HappyKnuckles Just checking with you on this. We're still manually patching this thanks to your hard work -- is the PR ready for review or is there something I could help with to un-block the team? Looks like it was ready to me (save maybe rebasing on main at this point) but just curious.

If it helps, I'd be happy to open a fresh PR with this rebased against main so you don't have to deal with the merge conflicts. Just let me know!

Sorry for leaving you in the dust. I had problems with the tests as CI was failing and locally it wasn't. However i believe this PR is not needed anymore as i think the problem was fixed as a side effect in this #31343.

@lincolnthree

Copy link
Copy Markdown

@HappyKnuckles Interesting. That does look like it should fix it! I'll test that out. Is that patch working for you if you've tried it?

@HappyKnuckles

Copy link
Copy Markdown
Author

@HappyKnuckles Interesting. That does look like it should fix it! I'll test that out. Is that patch working for you if you've tried it?

I ran my test suite and looked at the preview. Test suite passed and it looked like it is working correctly in the manual check.
I didn't verify in my own app tho.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: angular @ionic/angular package package: core @ionic/core package package: react @ionic/react package package: vue @ionic/vue package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: content does not update offset variables if header and footer dimensions change